/*
 * Copyright 2010 Gal Dolber.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.unnison.framework.rebind.binder;

import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPackage;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.event.dom.client.HasNativeEvent;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.rebind.StringSourceWriter;

import com.unnison.framework.client.GuitEntryPoint;
import com.unnison.framework.client.GuitView;
import com.unnison.framework.client.Implementation;
import com.unnison.framework.client.ImplicitCast;
import com.unnison.framework.client.binder.Attribute;
import com.unnison.framework.client.binder.BindingProvider;
import com.unnison.framework.client.binder.EventGetter;
import com.unnison.framework.client.binder.ViewAccesor;
import com.unnison.framework.client.binder.ViewField;
import com.unnison.framework.client.binder.ViewHandler;
import com.unnison.framework.client.eventbus.EventBus;
import com.unnison.framework.rebind.gin.GinOracle;
import com.unnison.framework.rebind.guitview.GuitViewGenerator;
import com.unnison.framework.rebind.guitview.GuitViewHelper;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

@Deprecated
public class ViewBinderGenerator extends EventBusBinderGenerator {

    private final GuitViewGenerator guitViewGenerator = new GuitViewGenerator();
    private static final String STRINGCANONICALNAME = String.class.getCanonicalName();

    protected JClassType bindingProviderType;
    protected JClassType hasNativeEventType;

    @Override
    protected void generate(SourceWriter writer) throws UnableToCompleteException {
        findContributors();

        hasNativeEventType = hasNativeEventType == null ? getType(HasNativeEvent.class.getCanonicalName()) : hasNativeEventType;

        // Do we need to check this?
        JClassType presenterType = baseClass.getImplementedInterfaces()[0].isParameterized().getTypeArgs()[0];

        // If it is an parameterized type we need to find the base type to get the right method names
        if (presenterType.isParameterized() != null) {
            presenterType = presenterType.isParameterized().getBaseType();
        }

        JClassType viewInterfaceType = baseClass.getImplementedInterfaces()[0].isParameterized().getTypeArgs()[1];

        // Validate return types of the view interface
        validateViewInterface(viewInterfaceType);

        // Look at the gin bindings for the view implementation
        String viewInterfaceName = viewInterfaceType.getQualifiedSourceName();

        JClassType guitViewType = getType(GuitView.class.getCanonicalName());

        JClassType viewType = null;
        String viewTypeName = null;
        if (viewInterfaceType.isAssignableTo(guitViewType)) {
            viewType = getType(viewTypeName = guitViewGenerator.generate(logger, context, viewInterfaceName));
        } else {
            String implementation = GinOracle.findImplementation(viewInterfaceName);
            if (implementation != null) {
                viewType = getType(implementation);
            } else if (viewInterfaceType.isAnnotationPresent(Implementation.class)) {
                viewType = getType(viewInterfaceType.getAnnotation(Implementation.class).value().getCanonicalName());
                if (!viewType.isAssignableTo(viewInterfaceType)) {
                    error("The view implementation '%s' should implement the view interface '%s'. Found: %s",
                        viewType.getQualifiedSourceName(), viewInterfaceName, viewInterfaceName);
                }
            } else {
                error("This view %s should be binded in gin or annotated with %s", viewInterfaceName,
                    Implementation.class.getCanonicalName());
            }
            viewTypeName = viewType.getQualifiedSourceName();
        }

        writer.println(bindingsDeclaration);

        writer.println(viewTypeName + " view;");

        // Field -> Type
        HashMap<String, JType> validBindingFieldsTypes = viewType != null ? getValidViewBindingFields(viewType)
            : getValidGuitViewBindingFields(viewInterfaceType);
        writer.println("public void bind(final " + presenterType.getQualifiedSourceName() + " presenter, final " + viewInterfaceName
            + " v, final " + EventBus.class.getCanonicalName() + " eventBus) {");
        writer.indent();
        printEventBusBindingMethods(writer, presenterType);
        writer.println("view = (" + viewTypeName + ")v;");
        printViewFieldBindings(writer, presenterType, viewType, viewInterfaceType, viewTypeName, validBindingFieldsTypes);
        SourceWriter bindingProviderUnbinds = new StringSourceWriter();
        printViewBindingMethods(writer, presenterType, viewType, viewInterfaceType, viewTypeName, bindingProviderUnbinds,
            validBindingFieldsTypes.keySet());
        printPresentersInitilizersCalls(writer, presenterType);
        writer.outdent();
        writer.println("}");

        writer.println("public void unbind() {");
        writer.indent();

        writer.println("if (bindings.size() == 0) { return; }");

        writer.println("for (" + handlerRegistrationName + " b : bindings) {b.removeHandler();}");
        writer.println("bindings.clear();");
        writer.println(bindingProviderUnbinds.toString());
        writer.outdent();
        writer.println("}");
    }

    private String getAddMethodName(String eventName) {
        return "add" + eventName.substring(0, 1).toUpperCase() + eventName.substring(1) + "Handler";
    }

    private HashMap<String, JType> getValidGuitViewBindingFields(JClassType viewInterfaceType) throws UnableToCompleteException {
        Set<String> findUiFields = GuitViewHelper.findUiFields(viewInterfaceType, logger, false);
        HashMap<String, JType> fields = new HashMap<String, JType>();
        for (String f : findUiFields) {
            String[] parts = f.split(" ");

            // Parameterized?
            String type = parts[1];
            int index = type.indexOf("<");
            if (index != (-1)) {
                type = type.substring(0, index);
            }

            fields.put(parts[2].substring(0, parts[2].length() - 1), getType(type));
        }
        return fields;
    }

    /**
     * Finds the valid binding fields.
     */
    private HashMap<String, JType> getValidViewBindingFields(JClassType viewType) {
        HashMap<String, JType> validFields = new HashMap<String, JType>();
        for (JField f : viewType.getFields()) {
            if (!f.isPrivate() && !f.isStatic()) {
                validFields.put(f.getName(), f.getType());
            }
        }
        return validFields;
    }

    private void printViewBindingMethods(SourceWriter writer, JClassType presenterType, JClassType viewType, JClassType viewInterfaceType,
        String viewTypeName, SourceWriter bindingProviderUnbinds, Set<String> validBindingFields) throws UnableToCompleteException {

        JPackage contextEventsPackage = getPackage(presenterType.getPackage().getName() + ".event");
        for (JMethod m : presenterType.getMethods()) {
            if (m.isAnnotationPresent(ViewHandler.class)) {
                String name = m.getName();

                validateHandler(m, name, presenterType.getQualifiedSourceName());

                String eventName;
                Set<String> bindingFields = null;
                if (name.startsWith("$")) {
                    // Direct view binding
                    eventName = name.substring(1);
                } else {
                    // View fields binding
                    String[] nameParts = name.split("[$]");

                    // Check the name format
                    if (nameParts.length < 2) {
                        error("The method %s on the class %s have a bad binding format. It should be: "
                            + "'{viewField}${eventName}' or for binding multiple fields: '{viewField1}${viewField2}${eventName}'", name,
                            presenterType.getQualifiedSourceName());
                    }

                    // Check that the binding fields are valid
                    bindingFields = new HashSet<String>();
                    for (int n = 0; n < nameParts.length - 1; n++) {
                        if (!validBindingFields.contains(nameParts[n])) {
                            error(
                                "The field %s on the class %s is not a valid binding field. It must be public or protected and not static",
                                nameParts[n], viewTypeName);
                        }
                        bindingFields.add(nameParts[n]);
                    }
                    eventName = nameParts[nameParts.length - 1]; // last token
                }

                // Check the event type and name convention
                JClassType eventType = getType(m.getAnnotation(ViewHandler.class).value().getCanonicalName());
                if (eventType.equals(gwtEventType)) {
                    eventType = getEventByName(eventName, contextEventsPackage);
                    if (eventType == null) {
                        error("There is no context, dom or shared event with the name '%s'. Binding method: '%s' in class: '%s'",
                            eventName, name, presenterType.getQualifiedSourceName());
                    }
                } else {
                    // Check that the method name correspond to the event type
                    String eventNameToEventClassName = eventNameToEventClassName(eventName);
                    if (!eventNameToEventClassName.equals(eventType.getSimpleSourceName())) {
                        error("The method '%s' in the class '%s' have a typo in the name. The last token should be : ..$%s() {.. ", name,
                            presenterType.getQualifiedSourceName(), eventName);
                    }
                }

                // Check if is a binding provider
                boolean isBindingProvider = false;
                JClassType bindingProviderData = null;

                // If viewType == null we are in a GuitView that cannot have bindingProviders
                if (bindingFields != null && viewType != null) {
                    for (String f : bindingFields) {
                        JField field = viewType.getField(f);
                        if (field.getType().isClassOrInterface().isAssignableTo(bindingProviderType)) {
                            if (bindingFields.size() > 1) {
                                error("You cannot bind multiple fields with a binding provider. Found: %s.%s",
                                    presenterType.getQualifiedSourceName(), m.getName());
                            }

                            // Binding provider data type
                            bindingProviderData = field.getType().isParameterized().getTypeArgs()[1];

                            isBindingProvider = true;

                            bindingProviderUnbinds.println("view." + f + ".unbindAll();");

                            break;
                        }
                    }
                }

                /**
                 * Find parameters bindings. The binding can be with the event(cannot have anidation of
                 * getters):'getter'->'getGetter()' or with the view:'$getter'->'view.getGetter()' or with a view field
                 * '{viewField$getter}'->'view.viewField.getGetter();', this last two ones will support anidation:
                 * '{viewField$getter$another}'->'view.viewField.getGetter().getAnother();'
                 **/
                StringBuilder bindingParameters = new StringBuilder();
                JParameter[] parameters = m.getParameters();
                ArrayList<String> parameterStrings = new ArrayList<String>();
                for (JParameter p : parameters) {
                    String parameter = p.getName();
                    JType parameterType = p.getType();
                    if (bindingParameters.length() > 0) {
                        bindingParameters.append(", ");
                    } else if (isBindingProvider) {
                        if (!parameter.equals("data")) {
                            error("The first parameter of an ViewHandler of a binding provider should be named 'data'. Found: %s.%s",
                                presenterType.getQualifiedSourceName(), m.getName());
                        }

                        if (!parameterType.equals(bindingProviderData)) {
                            error("The first parameter of the ViewHandler should be %s. Found %s.%s",
                                bindingProviderData.getQualifiedSourceName(), presenterType.getQualifiedSourceName(), m.getName());
                        }

                        bindingParameters.append("data");
                        continue;
                    }

                    int initlenght = bindingParameters.length();

                    // Implicit cast
                    bindingParameters.append("(" + parameterType.getErasedType().getParameterizedQualifiedSourceName() + ")");

                    String getter = "get";

                    // Check if it is a boolean then the getter is 'is' not 'get'
                    JPrimitiveType parameterTypeIsPrimitive = parameterType.isPrimitive();
                    if (parameterTypeIsPrimitive != null && parameterTypeIsPrimitive.equals(JPrimitiveType.BOOLEAN)) {
                        getter = "is";
                    }

                    // TODO Validate this? Compiler error logs are pretty clear.
                    if (p.isAnnotationPresent(Attribute.class)) {
                        // Only valid for domEvents
                        if (!eventType.isAssignableTo(hasNativeEventType)) {
                            error("Attributes binding are only valid for DomEvents. Found: %s.%s in parameter: %s",
                                presenterType.getQualifiedSourceName(), name, parameter);
                        }

                        String parameterTypeQualifiedSourceName = parameterType.getQualifiedSourceName();
                        boolean isString = parameterTypeQualifiedSourceName.equals(STRINGCANONICALNAME);
                        if (!isString) {
                            bindingParameters.append(parameterTypeQualifiedSourceName + ".valueOf(");
                        }
                        bindingParameters.append("((" + Element.class.getCanonicalName()
                            + ")event.getNativeEvent().getEventTarget().cast()).getAttribute(\"" + parameter + "\")");
                        if (!isString) {
                            bindingParameters.append(")");
                        }
                    } else if (parameter.indexOf("$") == -1) {
                        // Event binding
                        bindingParameters.append("event.");
                        bindingParameters.append(getter);
                        bindingParameters.append(capitalize(parameter));
                        bindingParameters.append("()");
                    } else if (p.isAnnotationPresent(EventGetter.class)) {
                        // Event binding anidated binding
                        String[] parameterParts = parameter.split("[$]");

                        bindingParameters.append("event");
                        for (int n = 0; n < parameterParts.length - 1; n++) {
                            bindingParameters.append(".get");
                            bindingParameters.append(capitalize(parameterParts[n]));
                            bindingParameters.append("()");
                        }

                        bindingParameters.append(".");
                        bindingParameters.append(getter);
                        bindingParameters.append(capitalize(parameterParts[parameterParts.length - 1]));
                        bindingParameters.append("()");
                    } else if (parameter.startsWith("$")) {
                        // Direct view binding

                        //Deprecated
                        warn("Injecting view getters is deprecated, use @ViewField instead. Found: %s.%s",
                            presenterType.getQualifiedSourceName(), name);

                        parameter = parameter.substring(1);
                        String[] parameterParts = parameter.split("[$]");

                        bindingParameters.append("view");
                        for (int n = 0; n < parameterParts.length - 1; n++) {
                            bindingParameters.append(".get");
                            bindingParameters.append(capitalize(parameterParts[n]));
                            bindingParameters.append("()");
                        }

                        bindingParameters.append(".");
                        bindingParameters.append(getter);
                        bindingParameters.append(capitalize(parameterParts[parameterParts.length - 1]));
                        bindingParameters.append("()");

                    } else {
                        // View field binding

                        //Deprecated
                        warn("Injecting view field's getters is deprecated, use @ViewField instead. Found: %s.%s",
                            presenterType.getQualifiedSourceName(), name);

                        String[] parameterParts = parameter.split("[$]");

                        bindingParameters.append("view.");
                        bindingParameters.append(parameterParts[0]);
                        for (int n = 1; n < parameterParts.length - 1; n++) {
                            bindingParameters.append(".get");
                            bindingParameters.append(capitalize(parameterParts[n]));
                            bindingParameters.append("()");
                        }

                        bindingParameters.append(".");
                        bindingParameters.append(getter);
                        bindingParameters.append(capitalize(parameterParts[parameterParts.length - 1]));
                        bindingParameters.append("()");
                    }

                    parameterStrings.add(bindingParameters.substring(initlenght, bindingParameters.length()));
                }

                // Get event handler name
                JClassType handlerType = getHandlerForEvent(eventType);
                if (handlerType == null) {
                    error("Parameter '%s' is not an event (subclass of GwtEvent).", eventType.getName());
                }

                // Retrieves the single method (usually 'onSomething') related to all
                // handlers. Ex: onClick in ClickHandler, onBlur in BlurHandler ...
                JMethod[] methods = handlerType.getMethods();
                if (methods.length != 1) {
                    error("'%s' has more than one method defined.", handlerType.getName());
                }

                // 'onSomething' method
                JMethod handlerOnMethod = methods[0];

                String methodName = m.getName();
                String handlerTypeName = handlerType.getQualifiedSourceName();

                // Write handler
                SourceWriter eventHandlerWriter = new StringSourceWriter();
                eventHandlerWriter.println("new " + handlerTypeName + "() {");
                eventHandlerWriter.indent();
                eventHandlerWriter.println("public void " + handlerOnMethod.getName() + "(final " + eventType.getQualifiedSourceName()
                    + " event) {");
                eventHandlerWriter.indent();

                String bindingParametersString = bindingParameters.toString();

                // Process contributors
                BinderContextImpl binderContext = processMethodContributors(presenterType, viewType, viewInterfaceType, viewTypeName, m,
                    eventType, parameterStrings.toArray(new String[parameterStrings.size()]));

                StringSourceWriter handlerWriter = new StringSourceWriter();

                handlerWriter.println("if (!GWT.isScript()) {");
                handlerWriter.println(GuitEntryPoint.class.getCanonicalName() + ".getLogger().initLog(\"VIEW " + binderContext.getLog()
                    + "\");");
                handlerWriter.println("}");

                handlerWriter.print("presenter." + methodName + "(");
                handlerWriter.print(bindingParametersString);
                handlerWriter.println(");");

                handlerWriter.println("if (!GWT.isScript()) {");
                handlerWriter.println(GuitEntryPoint.class.getCanonicalName() + ".getLogger().endLog();");
                handlerWriter.println("}");

                eventHandlerWriter.println(binderContext.build(handlerWriter));

                eventHandlerWriter.outdent();
                eventHandlerWriter.println("}");
                eventHandlerWriter.outdent();
                eventHandlerWriter.print("}");

                String addMethodName = getAddMethodName(eventName);
                if (isBindingProvider) {
                    JField field = viewType.findField(bindingFields.iterator().next());

                    // TODO Check this?
                    JClassType hasHandlerType = field.getType().isParameterized().getTypeArgs()[0];
                    String addEventMethodName = hasHandlerType.getMethods()[0].getName();
                    JClassType dataType = field.getType().isParameterized().getTypeArgs()[1];

                    writer.println("view." + field.getName() + " = new " + field.getType().getParameterizedQualifiedSourceName() + "() {");
                    writer.indent();

                    writer.println("public void bind(" + hasHandlerType.getQualifiedSourceName() + " widget, final "
                        + dataType.getQualifiedSourceName() + " data) {");
                    writer.indent();
                    writer.println("bindings.put(widget, widget." + addEventMethodName + "(" + eventHandlerWriter.toString() + "));");
                    writer.outdent();
                    writer.println("}");

                    writer.outdent();
                    writer.println("};");
                } else if (bindingFields != null) {
                    writer.print("final " + handlerTypeName + " " + methodName + " =");
                    writer.print(eventHandlerWriter.toString());
                    writer.println(";");

                    for (String f : bindingFields) {
                        writer.println("bindings.add(view." + f + "." + addMethodName + "(" + methodName + "));");
                    }
                } else {
                    writer.print("bindings.add(view." + addMethodName + "(" + eventHandlerWriter.toString() + "));");
                }
            }
        }
    }

    private void printViewFieldBindings(SourceWriter writer, JClassType presenterType, JClassType viewType, JClassType viewInterfaceType,
        String viewTypeName, HashMap<String, JType> validBindingFieldsType) throws UnableToCompleteException {

        Set<String> validBindingFields = validBindingFieldsType.keySet();

        JClassType viewAccesorType = getType(ViewAccesor.class.getCanonicalName());

        JField[] fields = presenterType.getFields();
        for (JField f : fields) {
            if (f.isAnnotationPresent(ViewField.class)) {
                String name = f.getName();
                String viewName = f.getAnnotation(ViewField.class).value();
                if (viewName.isEmpty()) {
                    viewName = name;
                }
                if (!validBindingFields.contains(viewName)) {
                    error("The field '%s' do not exists. Found: %s.%s", viewName, presenterType.getQualifiedSourceName(), name);
                }

                JClassType type = f.getType().isInterface();
                if (type.isAssignableTo(viewAccesorType)) {
                    if (!type.isAnnotationPresent(Implementation.class)) {
                        error("The ViewAccesor is not annotated with @Implementation. Found: %s", type.getQualifiedSourceName());
                    }
                    JClassType implementation = getType(type.getAnnotation(Implementation.class).value().getCanonicalName());
                    if (!implementation.isAssignableTo(type)) {
                        error("An implementation of a ViewAccesor must implement the ViewAccesor interface. Found: %s",
                            implementation.getQualifiedSourceName());
                    }
                    writer.println("{");
                    writer.indent();
                    writer.println(implementation.getQualifiedSourceName() + " accesor = new " + implementation.getQualifiedSourceName()
                        + "();");
                    writer.println("accesor.setTarget(view." + viewName + ");");
                    writer.println("presenter." + name + " = accesor;");
                    writer.outdent();
                    writer.print("}");
                    writer.println();
                } else if (type == null || type.isAssignableFrom(validBindingFieldsType.get(viewName).isClassOrInterface())) {
                    writer.println("presenter." + name + " = " + "(" + f.getType().getQualifiedSourceName() + ")" + "view." + viewName
                        + ";");
                    writer.println();
                } else {
                    // Interface emulation (without exceptions)
                    writer.println("presenter." + name + " = new " + type.getParameterizedQualifiedSourceName() + "() {");
                    writer.indent();

                    JMethod[] methods = type.getOverridableMethods();
                    for (JMethod m : methods) {
                        writer.print(m.getReadableDeclaration(false, true, true, true, true));
                        writer.println("{");
                        writer.indent();

                        // Find the parameters
                        StringBuilder callParameters = new StringBuilder();
                        for (JParameter p : m.getParameters()) {
                            if (callParameters.length() > 0) {
                                callParameters.append(", ");
                            }
                            if (p.isAnnotationPresent(ImplicitCast.class)) {
                                callParameters.append("(");
                                callParameters.append(p.getAnnotation(ImplicitCast.class).value().getCanonicalName());
                                callParameters.append(") ");
                            }
                            callParameters.append(p.getName());
                        }

                        JType returnType = m.getReturnType();
                        if (!returnType.equals(JPrimitiveType.VOID)) {
                            writer.print("return ");
                        }

                        writer.indent();
                        writer.println(createdClassName + ".this.view." + viewName + "." + m.getName() + "(" + callParameters.toString()
                            + ");");
                        writer.outdent();

                        writer.outdent();
                        writer.println("}");
                        writer.println();
                    }

                    // Get .equals working on emulated interfaces for event.getSource() comparations
                    writer.println("@Override");
                    writer.println("public boolean equals(Object obj) {");
                    writer.indent();
                    writer.println("return view." + viewName + ".equals(obj);");
                    writer.outdent();
                    writer.println("}");
                    writer.println();

                    writer.outdent();
                    writer.println("};");
                }
            }
        }
    }

    @Override
    protected void processComposer(ClassSourceFileComposerFactory composer) {
        super.processComposer(composer);
        bindingProviderType = getType(BindingProvider.class.getCanonicalName());
    }

    private void validateViewInterface(JClassType viewInterface) throws UnableToCompleteException {
        JMethod[] methods = viewInterface.getMethods();
        for (JMethod m : methods) {
            JPrimitiveType primitive = m.getReturnType().isPrimitive();
            if (primitive == null || !primitive.equals(JPrimitiveType.VOID)) {
                error("Guit do not allow to have getters in a view interface, always return void. Found: %s.%s",
                    viewInterface.getQualifiedSourceName(), m.getName());
            }
        }
    }
}
